【专知-PyTorch手把手深度学习教程07】NLP-基于字符级RNN的姓名分类
点击上方“专知”关注获取更多AI知识!
【导读】主题链路知识是我们专知的核心功能之一,为用户提供AI领域系统性的知识学习服务,一站式学习人工智能的知识,包含人工智能( 机器学习、自然语言处理、计算机视觉等)、大数据、编程语言、系统架构。使用请访问专知 进行主题搜索查看 - 桌面电脑访问www.zhuanzhi.ai, 手机端访问www.zhuanzhi.ai 或关注微信公众号后台回复" 专知"进入专知,搜索主题查看。值国庆佳节,专知特别推出独家特刊-来自中科院自动化所专知小组博士生huaiwen和Mandy创作的-PyTorch教程学习系列, 今日带来第五篇-< NLP系列(二) 基于字符级RNN的姓名分类 >
< NLP系列(二) 基于字符级RNN的姓名分类 >
< NLP系列(三) 基于字符级RNN的姓名生成 >
Practical PyTorch: 用字符集RNN进行名称分类
本文翻译自spro/practical-pytorch
原文:https://github.com/spro/practical-pytorch/blob/master/char-rnn-classification/char-rnn-classification.ipynb
翻译: Mandy
辅助: huaiwen
初始
我们将建立和训练一个基本的字符级RNN来分类单词。字符级RNN将字作为一系列字符读入 - 在每个步骤输出预测和“隐藏状态”,将其先前的隐藏状态馈送到每个下一步骤。我们将最终预测作为输出,即该词属于哪一类。具体来说,我们将从18种语言的起源开始列出数千个姓氏,并根据拼写预测该名字来源于哪种语言:
举例:
$ python predict.py Hinton
(-0.47) Scottish
(-1.52) English
(-3.57) Irish
$ python predict.py Schmidhuber
(-0.19) German
(-2.48) Czech
(-2.68) Dutch
推荐阅读
假设你至少安装了PyTorch,知道Python,并了解Tensors:
http://pytorch.org/ ( 有关安装说明的网址)
Deep Learning with PyTorch: A 60-minute Blitz (这个链接让你大致了解什么是PyTorch )
jcjohnson's PyTorch examples ( 深入了解PyTorch )
Introduction to PyTorch for former Torchies ( 如果你之前用过 Lua Torch )
知道并了解RNNs 以及它们是如何工作的是很有用的
The Unreasonable Effectiveness of Recurrent Neural Networks ( 展示了一堆现实生活中的例子)
Understanding LSTM Networks(是关于LSTM具体的,但也是关于RNN的一般介绍)
准备数据
包含在data/names目录中的是18个文本文件,名称为“[Language] .txt”。每个文件包含一堆名称,每行一个名称,主要是罗马字体化的(但是我们仍然需要从Unicode转换为ASCII)。
我们最终会得到一个每种语言名称列表的字典,{language:[names ...]}。通用变量“category”和“line”(在我们的例子中用于语言和名称)用于后续的可扩展性。
import glob
all_filenames = glob.glob('../data/names/*.txt')
print(all_filenames)
['../data/names/Arabic.txt', '../data/names/Chinese.txt', '../data/names/Czech.txt', '../data/names/Dutch.txt', '../data/names/English.txt', '../data/names/French.txt', '../data/names/German.txt', '../data/names/Greek.txt', '../data/names/Irish.txt', '../data/names/Italian.txt', '../data/names/Japanese.txt', '../data/names/Korean.txt', '../data/names/Polish.txt', '../data/names/Portuguese.txt', '../data/names/Russian.txt', '../data/names/Scottish.txt', '../data/names/Spanish.txt', '../data/names/Vietnamese.txt']
import unicodedata
import string
all_letters = string.ascii_letters + " .,;'"
n_letters = len(all_letters)
# Turn a Unicode string to plain ASCII, thanks to http://stackoverflow.com/a/518232/2809427
def unicode_to_ascii(s):
return ''.join(
c for c in unicodedata.normalize('NFD', s)
if unicodedata.category(c) != 'Mn'
and c in all_letters
)
print(unicode_to_ascii('Ślusàrski'))
Slusarski
# Build the category_lines dictionary, a list of names per language
category_lines = {}
all_categories = []
# Read a file and split into lines
def readLines(filename):
lines = open(filename).read().strip().split('\n')
return [unicode_to_ascii(line) for line in lines]
for filename in all_filenames:
category = filename.split('/')[-1].split('.')[0]
all_categories.append(category)
lines = readLines(filename)
category_lines[category] = lines
n_categories = len(all_categories)
print('n_categories =', n_categories)
n_categories = 18
现在我们有category_lines,一个将每个类别(语言)映射到行 列表(名称)的字典。我们还跟踪所有类别(只是一个语言列表)和n_categories以供以后参考。
print(category_lines['Italian'][:5])
['Abandonato', 'Abatangelo', 'Abatantuono', 'Abate', 'Abategiovanni']
把名字变成Tensors
现在我们已经组织了所有的名字,我们需要把它们变成Tensors来使用它们。
为了表示单个字母,我们使用大小为<1 x n_letters>的“one-hot vector”。一个热向量填充0,除了当前字母的索引1,例如“b”= <0 1 0 0 0 ...>。
为了表达我们的意思,我们将一大堆加入到2维矩阵<line_length x 1 x n_letters>中。
这个额外的1维是因为PyTorch假设一切都是批量的 -我们只是在这里使用批量大小为1。
import torch
# Just for demonstration, turn a letter into a <1 x n_letters> Tensor
def letter_to_tensor(letter):
tensor = torch.zeros(1, n_letters)
letter_index = all_letters.find(letter)
tensor[0][letter_index] = 1
return tensor
# Turn a line into a <line_length x 1 x n_letters>,
# or an array of one-hot letter vectors
def line_to_tensor(line):
tensor = torch.zeros(len(line), 1, n_letters)
for li, letter in enumerate(line):
letter_index = all_letters.find(letter)
tensor[li][0][letter_index] = 1
return tensor
print(letter_to_tensor('J'))
Columns 0 to 12
0 0 0 0 0 0 0 0 0 0 0 0 0
Columns 13 to 25
0 0 0 0 0 0 0 0 0 0 0 0 0
Columns 26 to 38
0 0 0 0 0 0 0 0 0 1 0 0 0
Columns 39 to 51
0 0 0 0 0 0 0 0 0 0 0 0 0
Columns 52 to 56
0 0 0 0 0
[torch.FloatTensor of size 1x57]
print(line_to_tensor('Jones').size())
torch.Size([5, 1, 57])
创建网络
在自动格式化之前,在Torch中创建一个循环神经网络涉及到克隆了多个时间步长的层的参数。这些层保持隐藏的状态和渐变,现在完全由图形本身处理。这意味着你可以以非常“纯净”的方式实施RNN,作为正常的前馈层。
这个RNN模块(大部分来自PyTorch for Torch用户教程的复制)只是2个线性层,它们在输入和隐藏状态下运行,输出后面有LogSoftmax层。
import torch.nn as nn
from torch.autograd import Variable
class RNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(RNN, self).__init__()
self.input_size = input_size
self.hidden_size = hidden_size
self.output_size = output_size
self.i2h = nn.Linear(input_size + hidden_size, hidden_size)
self.i2o = nn.Linear(input_size + hidden_size, output_size)
self.softmax = nn.LogSoftmax()
def forward(self, input, hidden):
combined = torch.cat((input, hidden), 1)
hidden = self.i2h(combined)
output = self.i2o(combined)
output = self.softmax(output)
return output, hidden
def init_hidden(self):
return Variable(torch.zeros(1, self.hidden_size))
手动测试网络
定义了我们定制的RNN类,我们可以创建一个新的实例:
n_hidden = 128
rnn = RNN(n_letters, n_hidden, n_categories)
为了开始运行这个网络,我们需要传递一个输入(在我们的例子中是当前字母的Tensor)和一个先前的隐藏状态(我们首先初始化为零)。我们将取回输出(每种语言的概率)和下一个隐藏状态(我们为下一步保留)。
请记住,PyTorch模块在变量上运行,而不是直接在Tensors。
input = Variable(letter_to_tensor('A'))
hidden = rnn.init_hidden()
output, next_hidden = rnn(input, hidden)
print('output.size =', output.size())
output.size = torch.Size([1, 18])
为了提高效率,我们不希望为每个步骤创建一个新的Tensor,所以我们将使用line_to_tensor而不是letter_to_tensor并使用slice。这可以通过预先计算批量的Tensors进一步优化。
input = Variable(line_to_tensor('Albert'))
hidden = Variable(torch.zeros(1, n_hidden))
output, next_hidden = rnn(input[0], hidden)
print(output)
Variable containing:
Columns 0 to 9
-2.8658 -2.8801 -2.7945 -2.9082 -2.8309 -2.9718 -2.9366 -2.9416 -2.7900 -2.8467
Columns 10 to 17
-2.9495 -2.9496 -2.8707 -2.8984 -2.8147 -2.9442 -2.9257 -2.9363
[torch.FloatTensor of size 1x18]
可以看到输出是<1 x n_categories> Tensor,其中每个项目都是该类别的可能性(更高的可能性)。
准备训练
在进行训练之前,我们应该制造一些功能函数。第一个是解释网络的输出,我们知道这是每个类别的可能性。我们可以使用Tensor.topk得到最大值的索引:
def category_from_output(output):
top_n, top_i = output.data.topk(1) # Tensor out of Variable with .data
category_i = top_i[0][0]
return all_categories[category_i], category_i
print(category_from_output(output))
('Irish', 8)
我们还需要一个快速的方式来获得训练示例(名称及其语言):
import random
def random_training_pair():
category = random.choice(all_categories)
line = random.choice(category_lines[category])
category_tensor = Variable(torch.LongTensor([all_categories.index(category)]))
line_tensor = Variable(line_to_tensor(line))
return category, line, category_tensor, line_tensor
for i in range(10):
category, line, category_tensor, line_tensor = random_training_pair()
print('category =', category, '/ line =', line)
category = Italian / line = Campana
category = Korean / line = Koo
category = Irish / line = Mochan
category = Japanese / line = Kitabatake
category = Vietnamese / line = an
category = Korean / line = Kwak
category = Portuguese / line = Campos
category = Vietnamese / line = Chung
category = Japanese / line = Ise
category = Dutch / line = Romijn
训练网络
现在,训练这个网络所需要的就是展示一大堆例子,让它做出猜测,并告诉它是否错误。
对于损耗函数nn.NLLLoss是适当的,因为RNN的最后一层是nn.LogSoftma
45 33918 45 15289 0 0 846 0 0:00:40 0:00:18 0:00:22 3288x。
criterion = nn.NLLLoss()
我们还将创建一个“优化器”,根据其梯度更新我们的模型的参数。我们将使用具有低学习率的SGD算法。
learning_rate = 0.005 # If you set this too high, it might explode. If too low, it might not learn optimizer = torch.optim.SGD(rnn.parameters(), lr=learning_rate)
每个训练循环将会:
创建输入和目标 tensors
创建一个归零的初始隐藏状态
阅读每个字母和保持下一个字母的隐藏状态
将最终输出与目标进行比较
反向传播
返回输出值和丢失值
def train(category_tensor, line_tensor):
rnn.zero_grad()
hidden = rnn.init_hidden()
for i in range(line_tensor.size()[0]):
output, hidden = rnn(line_tensor[i], hidden)
loss = criterion(output, category_tensor)
loss.backward()
optimizer.step()
return output, loss.data[0]
现在我们只需要运行一些例子。由于train函数返回输出和损失,我们可以打印其猜测,并跟踪绘制的损失。由于有1000个例子,我们只需打印每一个print_every时间步长,并且得到平均损失。
import time
import math
n_epochs = 100000
print_every = 5000
plot_every = 1000
# Keep track of losses for plotting
current_loss = 0
all_losses = []
def time_since(since):
now = time.time()
s = now - since
m = math.floor(s / 60)
s -= m * 60
return '%dm %ds' % (m, s)
start = time.time()
for epoch in range(1, n_epochs + 1):
# Get a random training input and target
category, line, category_tensor, line_tensor = random_training_pair()
output, loss = train(category_tensor, line_tensor)
current_loss += loss
# Print epoch number, loss, name and guess
if epoch % print_every == 0:
guess, guess_i = category_from_output(output)
correct = '✓' if guess == category else '✗ (%s)' % category
print('%d %d%% (%s) %.4f %s / %s %s' % (epoch, epoch / n_epochs * 100, time_since(start), loss, line, guess, correct))
# Add current loss avg to list of losses
if epoch % plot_every == 0:
all_losses.append(current_loss / plot_every)
current_loss = 0
5000 5% (0m 7s) 2.7940 Neil / Chinese ✗ (Irish)
10000 10% (0m 14s) 2.7166 O'Kelly / English ✗ (Irish)
15000 15% (0m 23s) 1.1694 Vescovi / Italian ✓
20000 20% (0m 31s) 2.1433 Mikhailjants / Greek ✗ (Russian)
25000 25% (0m 40s) 2.0299 Planick / Russian ✗ (Czech)
30000 30% (0m 48s) 1.9862 Cabral / French ✗ (Portuguese)
35000 35% (0m 55s) 1.5634 Espina / Spanish ✓
40000 40% (1m 5s) 3.8602 MaxaB / Arabic ✗ (Czech)
45000 45% (1m 13s) 3.5599 Sandoval / Dutch ✗ (Spanish)
50000 50% (1m 20s) 1.3855 Brown / Scottish ✓
55000 55% (1m 27s) 1.6269 Reid / French ✗ (Scottish)
60000 60% (1m 35s) 0.4495 Kijek / Polish ✓
65000 65% (1m 43s) 1.0269 Young / Scottish ✓
70000 70% (1m 50s) 1.9761 Fischer / English ✗ (German)
75000 75% (1m 57s) 0.7915 Rudaski / Polish ✓
80000 80% (2m 5s) 1.7026 Farina / Portuguese ✗ (Italian)
85000 85% (2m 12s) 0.1878 Bakkarevich / Russian ✓
90000 90% (2m 19s) 0.1211 Pasternack / Polish ✓
95000 95% (2m 25s) 0.6084 Otani / Japanese ✓
100000 100% (2m 33s) 0.2713 Alesini / Italian ✓
绘制结果
从 all_losses变量绘制的历史数据图展示网络学习:
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
%matplotlib inline
plt.figure()
plt.plot(all_losses)
[<matplotlib.lines.Line2D at 0x1103a9358>]
评估结果
要了解网络在不同类别上的运行情况,我们将创建一个混淆矩阵,表示对于每种实际语言(行),网络预测为哪种语言(列)的信息。(每一行表示这一类的数据在不同类别上的预测结果)
# Keep track of correct guesses in a confusion matrix
confusion = torch.zeros(n_categories, n_categories)
n_confusion = 10000
# Just return an output given a line
def evaluate(line_tensor):
hidden = rnn.init_hidden()
for i in range(line_tensor.size()[0]):
output, hidden = rnn(line_tensor[i], hidden)
return output
# Go through a bunch of examples and record which are correctly guessed
for i in range(n_confusion):
category, line, category_tensor, line_tensor = random_training_pair()
output = evaluate(line_tensor)
guess, guess_i = category_from_output(output)
category_i = all_categories.index(category)
confusion[category_i][guess_i] += 1
# Normalize by dividing every row by its sum
for i in range(n_categories):
confusion[i] = confusion[i] / confusion[i].sum()
# Set up plot
fig = plt.figure()
ax = fig.add_subplot(111)
cax = ax.matshow(confusion.numpy())
fig.colorbar(cax)
# Set up axes
ax.set_xticklabels([''] + all_categories, rotation=90)
ax.set_yticklabels([''] + all_categories)
# Force label at every tick
ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
plt.show()
你可以从主轴上选出亮点,显示哪些语言预测错误,例如很多汉语被预测为韩语这一类了,西班牙语被预测为意大利语。由这个图可知,希腊语预测的结果非常好,颜色最亮,英语预测的很差(可能的原因是和其他很多欧洲语言有很多重合的词)
在用户输入端运行
def predict(input_line, n_predictions=3):
print('\n> %s' % input_line)
output = evaluate(Variable(line_to_tensor(input_line)))
# Get top N categories
topv, topi = output.data.topk(n_predictions, 1, True)
predictions = []
for i in range(n_predictions):
value = topv[0][i]
category_index = topi[0][i]
print('(%.2f) %s' % (value, all_categories[category_index]))
predictions.append([value, all_categories[category_index]])
predict('Dovesky')
predict('Jackson')
predict('Satoshi')
Dovesky
(-0.87) Czech
(-0.88) Russian
(-2.44) Polish
Jackson
(-0.74) Scottish
(-2.03) English
(-2.21) Polish
Satoshi
(-0.77) Arabic
(-1.35) Japanese
(-1.81) Polish
Practical PyTorch repo中脚本的最终版本将上述代码分成几个文件:
data.py (loads files)
model.py (defines the RNN)
train.py (runs training)
predict.py (runs predict() with command line arguments)
server.py (serve prediction as a JSON API with bottle.py)
运行train.py来训练并保存网络。
运行具有名称的predict.py来查看预测:
$ python predict.py Hazaki
(-0.42) Japanese
(-1.39) Polish
(-3.51) Czech
运行server.py并访问http://localhost:5533 /Yourname 以获得JSON输出的预测。
明天继续推出:专知PyTorch深度学习教程系列< NLP系列(三) 基于字符级RNN的姓名生成 >,敬请关注。
完整系列搜索查看,请PC登录
www.zhuanzhi.ai, 搜索“PyTorch”即可得。
对PyTorch教程感兴趣的同学,欢迎进入我们的专知PyTorch主题群一起交流、学习、讨论,扫一扫如下群二维码即可进入:
了解使用专知-获取更多AI知识!
-END-
欢迎使用专知
专知,一个新的认知方式!目前聚焦在人工智能领域为AI从业者提供专业可信的知识分发服务, 包括主题定制、主题链路、搜索发现等服务,帮你又好又快找到所需知识。
使用方法>>访问www.zhuanzhi.ai, 或点击文章下方“阅读原文”即可访问专知
中国科学院自动化研究所专知团队
@2017 专知
专 · 知
关注我们的公众号,获取最新关于专知以及人工智能的资讯、技术、算法、深度干货等内容。扫一扫下方关注我们的微信公众号。
点击“阅读原文”,使用专知!